package wikibooks;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Static utility functions, such as HTML and MediaWiki page downloading.
 */
class Util {

    private static final String[] LaTeXCommandsArray = {"addcontentsline",
        "address", "addtocontents", "addtocounter", "addtolength",
        "addvspace", "alph", "Alph", "appendix", "arabic", "author",
        "backsalsh", "begin", "bfseries", "bibitem", "bibliography",
        "bibliographystyle", "bigskip", "caption", "cc", "cdots",
        "centering", "chapter", "circle", "cite", "cleardoublepage",
        "clearpage", "cline", "closing", "COLON", "dashbox", "date",
        "ddots", "depth", "documentclass", "dotfill", "emph", "encl",
        "end", "enlargethispage", "fbox", "flushbottom", "fnsymbol",
        "fontencoding", "fontfamily", "fontseries", "fontshape",
        "fontsize", "footnote", "footnotemark", "footnotesize",
        "footnotetext", "frac", "frame", "framebox", "fussy", "height",
        "hfill", "hline", "hrulefill", "hspace", "huge", "Huge",
        "hypenation", "include", "includeonly", "indent", "input", "item",
        "itshape", "kill", "label", "large", "Large", "LARGE", "ldots",
        "lefteqn", "line", "linebreak", "linethickness", "listoffigures",
        "listoftables", "location", "makebox", "makelabels", "maketitle",
        "markboth", "markright", "mathbf", "mathcal", "mathit",
        "mathnormal", "mathrm", "mathsf", "mathtt", "mathversion", "mbox",
        "mdseries", "medskip", "multicolumn", "multiput", "name",
        "newcommand", "newcounter", "newenvironment", "newfont",
        "newlength", "newline", "newpage", "newsavebox", "newtheorem",
        "nocite", "nofiles", "noindent", "nolinebreak", "nopagebreak",
        "normalfont", "normalsize", "onecolumn", "opening", "oval",
        "overbrace", "overline", "pagebreak", "pagenumbering", "pageref",
        "pagestyle", "par", "paragraph", "parbox", "poptabs", "ps",
        "pushtabs", "put", "raggedbottom", "raggedleft", "raggedright",
        "raisebox", "ref", "refstepcounter", "rmfamily", "roman", "Roman",
        "rule", "savebox", "sbox", "scriptsize", "scshape", "selectfont",
        "setcounter", "setlength", "settodepth", "settoheight",
        "settowidth", "sffamily", "shortstack", "signature", "sloppy",
        "slshape", "small", "smallskip", "sqrt", "startbreaks",
        "stepcounter", "stopbreaks", "subparagraph", "subsection",
        "subsubsection", "symbol", "tableofcontents", "telephone",
        "textbf", "textit", "textmd", "textnormal", "textrm", "textsc",
        "textsf", "textsl", "texttt", "textup", "thanks", "thispagestyle",
        "tiny", "title", "today", "totalheight", "ttfamily", "twocolumn",
        "typein", "typeout", "underbrace", "underline", "upshape",
        "usebox", "usecounter", "usefont", "usepackage", "value", "vdots",
        "vector", "verb", "vfill", "vline", "vspace", "width"
    };
    private static final Set<String> LaTeXCommands;

    static {
        LaTeXCommands = new HashSet<String>(LaTeXCommandsArray.length);
        for (String command : LaTeXCommandsArray) {
            LaTeXCommands.add(command);
        }
    }
    private static final String[] codeLanguagesArray = {"ABAP",
        "[R/2 4.3]ABAP", "[R/2 5.0]ABAP", "[R/2 5.0]ABAP", "[R/3 3.1]ABAP",
        "[R/3 4.6C]ABAP", "[R/3 6.10]ABAP", "ACSL", "Ada", "[83]Ada",
        "[95]Ada", "Ant", "Algol", "[60]Algol", "[68]Algol", "Assembler",
        "[x86masm]Assembler", "Awk", "[gnu]Awk", "[POSIX]Awk", "bash",
        "Basic", "[Visual]Basic", "C", "[ANSI]C", "[Handel]C",
        "[Objective]C", "[Sharp]C", "C++", "[ANSI]C++", "[GNU]C++",
        "[ISO]C++", "[Visual]C++", "Caml", "[light]Caml",
        "[Objective]Caml", "Clean", "Cobol", "[1974]Cobol", "[1985]Cobol",
        "[ibm]Cobol", "Comal 80", "csh", "Delphi", "Eiffel", "Elan",
        "erlang", "Euphoria", "Fortran", "[77]Fortran", "[90]Fortran",
        "[95]Fortran", "GCL", "Gnuplot", "Haskell", "HTML", "IDL",
        "[empty]IDL", "[CORBA]IDL", "inform", "Java", "[empty]Java",
        "[AspectJ]Java", "JVMIS", "ksh", "Lisp", "[empty]Lisp",
        "[Auto]Lisp", "Logo", "make", "[empty]make", "[gnu]make", "Matlab",
        "Mathematica", "[1.0]Mathematica", "[3.0]Mathematica", "Mercury",
        "MetaPost", "Miranda", "Mizar", "ML", "Modula-2", "MuPAD",
        "NASTRAN", "Oberon-2", "OCL", "[decorative]OCL", "[OMG]OCL",
        "Octave", "Oz", "Pascal", "[Borland6]Pascal", "[Standard]Pascal",
        "[XSC]Pascal", "Perl", "PHP", "PL/I", "Plasm", "POV", "Prolog",
        "Promela", "Python", "R", "Reduce", "Rexx", "RSL", "Ruby", "S",
        "[empty]S", "[PLUS]S", "SAS", "Scilab", "sh", "SHELXL", "Simula",
        "[67]Simula", "[CII]Simula", "[DEC]Simula", "[IBM]Simula", "SQL",
        "tcl", "[empty]tcl", "[tk]tcl", "TeX", "[AlLaTeX]TeX",
        "[common]TeX", "[LaTeX]TeX", "[plain]TeX", "[primitive]TeX",
        "VBScript", "Verilog", "VHDL", "[empty]VHDL", "[AMS]VHDL", "VRML",
        "[97]VRML", "XML", "XSLT"
    };
    private static final ArrayList<String> codeLanguages;

    static {
        codeLanguages = new ArrayList<String>(codeLanguagesArray.length);
        for (String language : codeLanguagesArray) {
            codeLanguages.add(language);
        }
    }
    private static HashMap<String, String> codeLanguageMap;

    static {
        /*
         * actionscript, ada, apache, applescript, asm, asp, autoit, bash,
         * blitzbasic, bnf, c, c_mac, caddcl, cadlisp, cfdg, cfm, cpp, cpp-qt,
         * csharp, css, d, delphi, diff, div, dos, eiffel, fortran, freebasic,
         * gml, groovy, html4strict, idl, ini, inno, io, java, java5,
         * javascript, latex, lisp, lua, matlab, mirc, mpasm, mysql, nsis, objc,
         * ocaml, ocaml-brief, oobas, oracle8, pascal, perl, php, php-brief,
         * plsql, python, qbasic, rails, reg, robots, ruby, sas, scheme,
         * sdlbasic, smalltalk, smarty, sql, tcl, text, thinbasic, tsql, vb,
         * vbnet, vhdl, visualfoxpro, winbatch, xml, xpp, z80
         */

        codeLanguageMap = new HashMap<String, String>();
        codeLanguageMap.put("asp", "VBScript");
        codeLanguageMap.put("cpp", "C++");
        codeLanguageMap.put("csharp", "[Sharp]C");
        codeLanguageMap.put("latex", "[LaTeX]TeX");
        codeLanguageMap.put("mysql", "SQL");
        codeLanguageMap.put("objc", "[Objective]C");
        codeLanguageMap.put("ocaml", "[Objective]Caml");
        codeLanguageMap.put("plsql", "SQL");
        codeLanguageMap.put("rails", "Ruby");
        codeLanguageMap.put("scheme", "[Auto]Lisp");
        codeLanguageMap.put("tsql", "SQL");
        codeLanguageMap.put("vb", "[Visual]Basic");
    }

    /** en.wikibooks.org, pl.wikibooks.org or something */
    private static String server = "en.wikibooks.org";

    /** The directory where the index.php is located (including beginning and ending slashes), usually /w/ */
    private static String mainDir = "/w/";
    
    private static CacheProvider cacheProvider = new CacheProviderImpl();

    /**
     * Set-up the URL to the wiki server (such as en.wikibooks.org). The parameter is not checked.
     * @param serverURL the server URL
     */
    public static void setServer(String serverURL) {
        server = serverURL;
    }

    /**
     * Set-up the main directory where the index.php file is located. The directory path is not checked,
     * must begin and end with a slash.
     * @param dirPath path to the index.php file.
     */
    public static void setMainDir(String dirPath){
        mainDir = dirPath;
    }

    public static Set<String> LaTeXCommands() {
        return new HashSet<String>(LaTeXCommands);
    }

    public static List<String> codeLanguages() {
        return new ArrayList<String>(codeLanguages);
    }

    public static Map<String, String> codeLanguageMap() {
        return new HashMap<String, String>(codeLanguageMap);
    }

    /**
     * returns language name for LaTeX code listing
     * @param codeLanguage language name from MediaWiki <code>&lt;source&gt;</code> tag
     * @return language name used in LaTeX code listing or null if <emph>codeLanguage</emph>
     * does not match any valid language code
     */
    public static String getValidCodeLanguage(String codeLanguage) {
        if (codeLanguages.toString().matches(".*((?i)" + codeLanguage + ").*")) // if lang valid
        {
            return codeLanguage;
        } else if (codeLanguageMap.containsKey(codeLanguage)) // if codeLanguage can be converted to a valid language
        {
            return codeLanguageMap.get(codeLanguage);
        } else {
            return null;
        }
    }

    /**
     * writes string to file using UTF8 encoding
     * @param text string to write
     * @param filename name of file where to write
     */
    public static void writeToFile(String text, String filename) {
        writeToFile(text, new File(filename));
    }

    /**
     * writes string to file using UTF8 encoding
     * @param text string to write
     * @param file file where to write
     */
    public static void writeToFile(String text, File file) {
        writeToFile(text, "UTF8", file);
    }

    public static void writeToFile(String text, String charSet, File file) {
        try {
            if (file.getParentFile() != null) {
                file.getParentFile().mkdirs();
            }

            OutputStream out = new PrintStream(file);
            BufferedWriter outWriter = new BufferedWriter(
                    new OutputStreamWriter(out, charSet));
            outWriter.write(text);
            outWriter.close();
        } catch (UnsupportedEncodingException e) {
            Log.addError("Could not write to file " + file.getName() + "; " + charSet + " is a bad encoding. (Try \"UTF8\" for " + "chapters and \"Latin1\" for images.)");
        } catch (Throwable e) {
            Logger.getLogger("Util").log(Level.SEVERE, "", e);
            Log.addError("Could not write to file " + file.getName() + "; Misc error:\n\n" + e.getMessage());
        }
    }

    public static void writeToFile(byte[] bytes, File file) {
        FileOutputStream out = null;
        try {
            if (file.getParentFile() != null) {
                file.getParentFile().mkdirs();
            }
            out = new FileOutputStream(file);
            out.write(bytes);
        } catch (IOException ex) {
            Logger.getLogger("Util").log(Level.SEVERE, null, ex);
        } finally {
            try {
                out.close();
            } catch (IOException ex) {
                Logger.getLogger("Util").log(Level.SEVERE, null, ex);
            }
        }
    }

    public static String getStringFromFile(String filename) {
        return getStringFromFile(new File(filename));
    }

    public static String getStringFromFile(File file) {
        StringBuilder retval = new StringBuilder();
        try {
            /*BufferedReader br = new BufferedReader(new FileReader(file));
            String line;
            while ((line = br.readLine()) != null) {
            retval.append(line);
            }*/
            InputStream is = new FileInputStream(file);
            byte[] bytes = new byte[6000];
            int byteCount;
            while ((byteCount = is.read(bytes)) > 0) {
                retval.append(new String(bytes, 0, byteCount, "UTF8"));
            }
        } catch (IOException ex) {
            Logger.getLogger("Util").log(Level.SEVERE, "", ex);
        }
        return retval.toString();
    }

    public static String getHTML(String url, String charSet) {
        try {
            Logger.getLogger("Util.transfer").info("downoading from " + url);
            return getHTML(new URL(url), charSet);
        } catch (MalformedURLException e) {
            Log.addError(url + " is a bad URL");
            return null;
        }
    }

    public static String getHTML(URL url, String charSet) {
        try {
            return new String(getBytes(url), charSet);
        } catch (UnsupportedEncodingException e) {
            Logger.getLogger("Util").log(Level.SEVERE, "", e);
            Log.addError("Could not get content from URL " + url.toString() + "; " + charSet + " is a bad encoding. (Try \"UTF8\" for " + "chapters and \"Latin1\" for images.)");
            return null;
        }
    }

    public static byte[] getBytes(String url) {
        try {
            Logger.getLogger("Util.transfer").info("downoading from " + url);
            return getBytes(new URL(url));
        } catch (MalformedURLException e) {
            Log.addError(url + " is a bad URL");
            return null;
        }
    }

    public static byte[] getBytes(URL url) {
        try {
            URLConnection urlConnection = url.openConnection();
            BufferedInputStream in = new BufferedInputStream(urlConnection.getInputStream());

            int bufLen = urlConnection.getContentLength();
            bufLen = bufLen > 0 ? bufLen : 1024;
            ByteArrayOutputStream bytes = new ByteArrayOutputStream(bufLen);
            for (int i = 0,  nextChar = in.read(); nextChar != -1; i++, nextChar = in.read()) {
                bytes.write(nextChar);
            }

            return bytes.toByteArray();
        } catch (IOException e) {
            Logger.getLogger("Util").log(Level.SEVERE, "error downloading from " + url.toString(), e);
            return new byte[0];
        }
    }

    /**
     * changes pageName to a safe string which can serve as a part of URL
     * @param pageName page name
     * @return safe page name
     */
    public static String toSafePageName(String pageName) {
        String safe = pageName.trim().replace(" ", "_");

        try {
            safe = URLEncoder.encode(pageName, "UTF8");
        } catch (UnsupportedEncodingException e) {
            Logger.getLogger("Util").log(Level.SEVERE, "", e);
        }
        return safe;
    }

    public static String downloadWikicode(String pageName) {
        return downloadWikicode(server, mainDir, pageName);
    }
    
    private static String downloadWikicodeCacheAware(String url) {
        String wikicode;
        if (cacheProvider.hasInCache(url)) {
            return cacheProvider.get(url);
        }
        
        wikicode = getHTML(url, "UTF8");
        if (wikicode != null) {
            cacheProvider.put(url, wikicode);
        }
        return wikicode;
    }

    public static String downloadWikicode(String site, String directory, String pageName) {
        site = site.replace("http://", "");
        pageName = toSafePageName(pageName);

        String url = "http://" + site + directory + "index.php?title=" + pageName + "&action=edit";

        String wikicode = downloadWikicodeCacheAware(url);

        if (wikicode != null && wikicode.contains("<textarea") && wikicode.contains("</textarea>")) {
            wikicode = wikicode.substring(wikicode.indexOf(">", wikicode.indexOf("<textarea")) + 1,
                    wikicode.indexOf("</textarea"));
        } else {
            Log.addError("Could not find <textarea ...> ... wikicode ... </textarea> on page " + pageName + "\n");
        }

        if (wikicode != null && wikicode.trim().length() == 0) {
            Log.addError(pageName + " is a red link (created anyway)" + "\n");
        }

        if (wikicode != null) {
            // convert some Latin 1 char names
            wikicode = wikicode.replace("&lt;", "<");
            wikicode = wikicode.replace("&gt;", ">");
            wikicode = wikicode.replace("&amp;", "&");
            wikicode = wikicode.replace("&quot;", "\"");
            wikicode = wikicode.replace("&ldquo;", "\"");
            wikicode = wikicode.replace("&rdquo;", "\"");
        }

        return wikicode;
    }

    public static String downloadTemplate(String title) {
        return downloadWikicode("Template:" + title);
    }

    public static String convertTemplateArguments(String template) {
        template = template.replace("\\{\\{\\{", "{{{");
        template = template.replace("\\}\\}\\}", "}}}");

        for (int i = 1; i < 10; i++) {
            template = template.replace("{{{" + i + "}}}", "#" + i);
            template = template.replace("{{{" + i + "|}}}", "#" + i);
        }

        return template;
    }

    /**
     * removes accents from given string, so "ąęć" becomes "aec".
     * @param withAccents string with accents
     * @return string with accents replaced with their ANSI characters equivalents
     */
    public static String removeAccents(String withAccents) {
        withAccents = withAccents.replace("ł", "l"); // normalizer does this wrong
        withAccents = withAccents.replace("Ł", "L"); // normalizer does this wrong
        //String temp = Normalizer.normalize(s, Normalizer.DECOMP, 0); // for Java < 6
        String temp = Normalizer.normalize(withAccents, java.text.Normalizer.Form.NFD); // for Java >= 6
        return temp.replaceAll("[^\\p{ASCII}]", "");
    }

    /**
     * removes all dangerous signs from string (spaces and accents).
     */
    public static String toSafeFilename(String s) {
        s = s.trim();
        s = s.replace(":", "");
        s = s.replace("?", "");
        s = s.replace(" ", "_");
        return removeAccents(s);
    }

    /**
     * changes image name to name safe for LaTeX. This means removing all dots
     * before extension, changing png to svg (SVG is converted to svg) and
     * lowercasing "PNG".
     * @param image full filename of the image, with extension, but without path (eg. "Wikibooks logo.svg")
     * @return file name which can be used to save file on disk
     */
    public static String toSafeImageName(String image) {
        image = toSafeFilename(image);
        image = image.replaceAll("\\.(svg|SVG|PNG)$", ".png");
        Pattern p = Pattern.compile("^(.+?)(\\.(png|PNG|svg|SVG|jpg|JPG|jpeg|JPEG|gif|GIF))");
        Matcher m = p.matcher(image);
        if (!m.find()) {
            Logger.getLogger("Util").severe(String.format("cannot find image name and extenseion for '%s'", image));
            return image;
        }
        String imageName = m.group(1);
        String imageExtension = m.group(2);
        imageName = imageName.replace(".", "_"); // LaTeX does not allow dots before extension

        return imageName + imageExtension;
    }

    /**
     * Converts:
     * 
     * <ul>
     * <li><code>(String)null</code> to <code>(String[])null</code></li>
     * <li>"" to {""}</li>
     * <li>"\n" to {""}</li>
     * <li>"foo" to {"foo"}</li>
     * <li>"foo\n" to {"foo"}</li>
     * <li>"foo\nbar" to {"foo", "bar"}</li>
     * <li>"foo\nbar\n" to {"foo", "bar"}</li>
     * <li>"\nfoo" to {"", "foo"}</li>
     * </ul>
     * 
     * @param lines
     *            multiline String
     * @return Array of lines in String lines
     */
    public static String[] lineArray(String lines) {
        if (lines == null) {
            return null;
        }

        ArrayList<String> lineArray = new ArrayList<String>();

        if (lines.length() == 0) {
            lineArray.add(lines);
        }
        
        lines = lines.replace("\r\n", "\n").replace("\r", "");

        while (lines.contains("\n")) {
            int newLineIndex = lines.indexOf("\n");
            String line = lines.substring(0, newLineIndex);
            /*if (line.endsWith("\r")) {
                line = line.substring(0, line.lastIndexOf("\r"));
            }*/
            
            lineArray.add(line);
            lines = lines.substring(newLineIndex + 1);
        }

        if (lines.length() > 0) {
            lineArray.add(lines);
        }
        return lineArray.toArray(new String[]{});
    }
}
